// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.CommandLine;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.BuildServer;
using Microsoft.DotNet.Cli.Commands;
using Microsoft.DotNet.Cli.Commands.BuildServer.Shutdown;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.Tools.Test.Utilities;
using Microsoft.Extensions.EnvironmentAbstractions;
using Moq;
using Parser = Microsoft.DotNet.Cli.Parser;

namespace Microsoft.DotNet.Tests.Commands
{
    public class BuildServerShutdownCommandTests : SdkTest
    {
        public BuildServerShutdownCommandTests(ITestOutputHelper log) : base(log)
        {
        }

        private readonly BufferedReporter _reporter = new();

        [Fact]
        public void GivenNoOptionsItEnumeratesAllServers()
        {
            var provider = new Mock<IBuildServerProvider>(MockBehavior.Strict);

            provider
                .Setup(p => p.EnumerateBuildServers(ServerEnumerationFlags.All))
                .Returns(Array.Empty<IBuildServer>());

            var command = CreateCommand(serverProvider: provider.Object);

            command.Execute().Should().Be(0);

            _reporter.Lines.Should().Equal(CliCommandStrings.NoServersToShutdown.Green());

            provider.Verify(p => p.EnumerateBuildServers(ServerEnumerationFlags.All), Times.Once);
        }

        [Fact]
        public void GivenMSBuildOptionOnlyItEnumeratesOnlyMSBuildServers()
        {
            var provider = new Mock<IBuildServerProvider>(MockBehavior.Strict);

            provider
                .Setup(p => p.EnumerateBuildServers(ServerEnumerationFlags.MSBuild))
                .Returns(Array.Empty<IBuildServer>());

            var command = CreateCommand(options: ["--msbuild"], serverProvider: provider.Object);

            command.Execute().Should().Be(0);

            _reporter.Lines.Should().Equal(CliCommandStrings.NoServersToShutdown.Green());

            provider.Verify(p => p.EnumerateBuildServers(ServerEnumerationFlags.MSBuild), Times.Once);
        }

        [Fact]
        public void GivenVBCSCompilerOptionOnlyItEnumeratesOnlyVBCSCompilers()
        {
            var provider = new Mock<IBuildServerProvider>(MockBehavior.Strict);

            provider
                .Setup(p => p.EnumerateBuildServers(ServerEnumerationFlags.VBCSCompiler))
                .Returns(Array.Empty<IBuildServer>());

            var command = CreateCommand(options: ["--vbcscompiler"], serverProvider: provider.Object);

            command.Execute().Should().Be(0);

            _reporter.Lines.Should().Equal(CliCommandStrings.NoServersToShutdown.Green());

            provider.Verify(p => p.EnumerateBuildServers(ServerEnumerationFlags.VBCSCompiler), Times.Once);
        }

        [Fact]
        public void GivenRazorOptionOnlyItEnumeratesOnlyRazorServers()
        {
            var provider = new Mock<IBuildServerProvider>(MockBehavior.Strict);

            provider
                .Setup(p => p.EnumerateBuildServers(ServerEnumerationFlags.Razor))
                .Returns(Array.Empty<IBuildServer>());

            var command = CreateCommand(options: ["--razor"], serverProvider: provider.Object);

            command.Execute().Should().Be(0);

            _reporter.Lines.Should().Equal(CliCommandStrings.NoServersToShutdown.Green());

            provider.Verify(p => p.EnumerateBuildServers(ServerEnumerationFlags.Razor), Times.Once);
        }

        [Fact]
        public void GivenSuccessfulShutdownsItPrintsSuccess()
        {
            var mocks = new[] {
                CreateServerMock("first"),
                CreateServerMock("second"),
                CreateServerMock("third")
            };

            var provider = new Mock<IBuildServerProvider>(MockBehavior.Strict);
            provider
                .Setup(p => p.EnumerateBuildServers(ServerEnumerationFlags.All))
                .Returns(mocks.Select(m => m.Object));

            var command = CreateCommand(serverProvider: provider.Object);

            command.Execute().Should().Be(0);

            _reporter.Lines.Should().Equal(
                FormatShuttingDownMessage(mocks[0].Object),
                FormatShuttingDownMessage(mocks[1].Object),
                FormatShuttingDownMessage(mocks[2].Object),
                FormatSuccessMessage(mocks[0].Object),
                FormatSuccessMessage(mocks[1].Object),
                FormatSuccessMessage(mocks[2].Object));

            VerifyShutdownCalls(mocks);
        }

        [Fact]
        public void GivenAFailingShutdownItPrintsFailureMessage()
        {
            const string FirstFailureMessage = "first failed!";
            const string ThirdFailureMessage = "third failed!";

            var mocks = new[] {
                CreateServerMock("first", exceptionMessage: FirstFailureMessage),
                CreateServerMock("second"),
                CreateServerMock("third", exceptionMessage: ThirdFailureMessage)
            };

            var provider = new Mock<IBuildServerProvider>(MockBehavior.Strict);
            provider
                .Setup(p => p.EnumerateBuildServers(ServerEnumerationFlags.All))
                .Returns(mocks.Select(m => m.Object));

            var command = CreateCommand(serverProvider: provider.Object);

            command.Execute().Should().Be(1);

            _reporter.Lines.Should().Equal(
                FormatShuttingDownMessage(mocks[0].Object),
                FormatShuttingDownMessage(mocks[1].Object),
                FormatShuttingDownMessage(mocks[2].Object),
                FormatFailureMessage(mocks[0].Object, FirstFailureMessage),
                FormatSuccessMessage(mocks[1].Object),
                FormatFailureMessage(mocks[2].Object, ThirdFailureMessage));

            VerifyShutdownCalls(mocks);
        }

        [Fact(Skip = "https://github.com/dotnet/sdk/issues/3684")]
        public void GivenARunningRazorServerItShutsDownSuccessfully()
        {
            var pipeName = Path.GetRandomFileName();

            var pidDirectory = _testAssetsManager.CreateTestDirectory(identifier: "pidDirectory").Path;

            var testInstance = _testAssetsManager
                .CopyTestAsset("TestRazorApp")
                .WithSource();

            new BuildCommand(testInstance)
                .WithEnvironmentVariable(BuildServerProvider.PidFileDirectoryVariableName, pidDirectory)
                .Execute($"/p:_RazorBuildServerPipeName={pipeName}")
                .Should()
                .Pass();

            var files = Directory.GetFiles(pidDirectory, RazorPidFile.FilePrefix + "*");
            files.Length.Should().Be(1);

            var pidFile = RazorPidFile.Read(new FilePath(files.First()));
            pidFile.PipeName.Should().Be(pipeName);

            new BuildServerCommand(Log)
                .WithWorkingDirectory(testInstance.TestRoot)
                .WithEnvironmentVariable(BuildServerProvider.PidFileDirectoryVariableName, pidDirectory)
                .Execute("shutdown", "--razor")
                .Should()
                .Pass()
                .And
                .HaveStdOutContaining(
                    string.Format(
                        CliCommandStrings.ShutDownSucceededWithPid,
                        CliStrings.RazorServer,
                        pidFile.ProcessId));
        }

        private BuildServerShutdownCommand CreateCommand(
            ReadOnlySpan<string> options = default,
            IBuildServerProvider serverProvider = null,
            IEnumerable<IBuildServer> buildServers = null,
            ServerEnumerationFlags expectedFlags = ServerEnumerationFlags.None)
        {
            ParseResult result = Parser.Parse(["dotnet", "build-server", "shutdown", .. options]);
            return new BuildServerShutdownCommand(
                result: result,
                serverProvider: serverProvider,
                useOrderedWait: true,
                reporter: _reporter);
        }

        private Mock<IBuildServer> CreateServerMock(string name, int pid = 0, string exceptionMessage = null)
        {
            var mock = new Mock<IBuildServer>(MockBehavior.Strict);

            mock.SetupGet(s => s.ProcessId).Returns(pid);
            mock.SetupGet(s => s.Name).Returns(name);

            if (exceptionMessage == null)
            {
                mock.Setup(s => s.Shutdown());
            }
            else
            {
                mock.Setup(s => s.Shutdown()).Throws(new Exception(exceptionMessage));
            }

            return mock;
        }

        private void VerifyShutdownCalls(IEnumerable<Mock<IBuildServer>> mocks)
        {
            foreach (var mock in mocks)
            {
                mock.Verify(s => s.Shutdown(), Times.Once);
            }
        }

        private static string FormatShuttingDownMessage(IBuildServer server)
        {
            if (server.ProcessId != 0)
            {
                return string.Format(CliCommandStrings.ShuttingDownServerWithPid, server.Name, server.ProcessId);
            }
            return string.Format(CliCommandStrings.ShuttingDownServer, server.Name);
        }

        private static string FormatSuccessMessage(IBuildServer server)
        {
            if (server.ProcessId != 0)
            {
                return string.Format(CliCommandStrings.ShutDownSucceededWithPid, server.Name, server.ProcessId).Green();
            }
            return string.Format(CliCommandStrings.ShutDownSucceeded, server.Name).Green();
        }

        private static string FormatFailureMessage(IBuildServer server, string message)
        {
            if (server.ProcessId != 0)
            {
                return string.Format(CliCommandStrings.ShutDownFailedWithPid, server.Name, server.ProcessId, message).Red();
            }
            return string.Format(CliCommandStrings.ShutDownFailed, server.Name, message).Red();
        }
    }
}
